package com.icehelper.hades.base.log;

import static java.util.Collections.EMPTY_LIST;
import static com.google.common.collect.Lists.newArrayList;
import static org.apache.commons.lang3.StringUtils.indexOf;
import static org.apache.commons.lang3.StringUtils.length;

import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;

/**
 * 日志layout
 * 格式化日志输出
 * @author Leon Sun
 */
@Plugin(name = "HadesLogLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class HadesLogLayout extends AbstractStringLayout {

  public static final String DELIMITER = "||";
  public static final String UNDEF = "_undef";
  private int lengthLimit = 0;
  private static final String DEFAULT_CONFIG_FILE = "desensitization.yml";
  private static Boolean desensitionFlag = false;
  private static Boolean inited = false;
  private static DesensitizationManager desensitizationManager;


  protected HadesLogLayout(Charset charset, Integer limit) {
    super(charset);
    if (limit != null && limit > 0) {
      this.lengthLimit = limit;
    }
  }

  @PluginFactory
  public static HadesLogLayout createLayout(
      @PluginAttribute(value = "lengthLimit") Integer lengthLimit,
      @PluginAttribute(value = "charset", defaultString = "UTF-8") Charset charset) {
    if (!inited) {
      getDesensitizationFlag();
    }
    return new HadesLogLayout(charset, lengthLimit);
  }

  private static void getDesensitizationFlag() {
    try (InputStream is = DesensitizationManager.class.getClassLoader()
        .getResourceAsStream(DEFAULT_CONFIG_FILE)) {
      inited = true;
      if (is != null && is.read(new byte[1024]) != -1) {
        desensitionFlag = true;
        desensitizationManager = new DesensitizationManager();
      }
    } catch (Exception e) {
      LOGGER.error("caught error when initialize desensitization mgr|error=", e);
    }
  }


  @Override
  public String toSerializable(LogEvent event) {
    StringBuffer resultBuffer = new StringBuffer();
    // 日志头时间为UTC时间格式
    SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    /**
     * 滴滴日志规范
     * 1、日志头部： 包含三个字段，用方括号（"["和"]"）包裹，分别是日志级别、时间（yyyy-MM-ddTHH:mm:ss.SSS+0800）、日志定位（上图为代码行数）
     * 2、日志类型： 又称为dltag，可枚举。用于标识日志在调用链路上所处环节。例如，“_undef”标识普通日志，“_com_http_success”表示http调用成功返回，“_com_request_in”表示被调用方收到请求
     * 3、日志KV：   日志附加信息，以“key=value”的形式出现，“||”作为分隔符。traceid和spanid是所有请求不可缺省的，cspanid是rpc请求日志不可缺省的
     */

    // 构建日志头部中的日志级别
    resultBuffer.append("[").append(event.getLevel()).append("]");

    // 构建日志头部中的日志时间
    resultBuffer.append('[').append(utcFormat.format(System.currentTimeMillis())).append("]");

    // 加入日志的类和行号
    String fullClassName = event.getSource().getClassName();
    resultBuffer.append('[')
        .append(fullClassName)
        .append(':')
        .append(event.getSource().getLineNumber())
        .append(']');

    // 构建日志类型DLTAG
    Map<String, String> contextDataMap = new HashMap<>(event.getContextData().toMap());
    resultBuffer.append(" ");
    String dlTag = contextDataMap.get(LogConstant.KEY_DL_TAG);
    if (StringUtils.isNotEmpty(dlTag)) {
      resultBuffer.append(dlTag).append(DELIMITER);
      contextDataMap.remove(LogConstant.KEY_DL_TAG);
    } else {
      resultBuffer.append(UNDEF).append(DELIMITER);
    }

    // 加入traceId
    String traceId = contextDataMap.get(LogConstant.KEY_TRACE_ID);
    if (StringUtils.isNotEmpty(traceId)) {
      resultBuffer.append("traceid=").append(traceId).append(DELIMITER);
      contextDataMap.remove(LogConstant.KEY_TRACE_ID);
    }

    // 加入spanId
    String spanId = contextDataMap.get(LogConstant.KEY_SPAN_ID);
    if (StringUtils.isNotEmpty(spanId)) {
      resultBuffer.append("spanid=").append(spanId).append(DELIMITER);
      contextDataMap.remove(LogConstant.KEY_SPAN_ID);
    }

    // 加入cspanId
    String cspanId = contextDataMap.get(LogConstant.KEY_CHILD_SPAN_ID);
    if (StringUtils.isNotEmpty(cspanId)) {
      resultBuffer.append("cspanid=").append(cspanId).append(DELIMITER);
      contextDataMap.remove(LogConstant.KEY_CHILD_SPAN_ID);
    }

    // 加入KV键值对
    contextDataMap
        .forEach((k, v) -> resultBuffer.append(k).append("=").append(v).append(DELIMITER));

    // 加入msg
    String msg = event.getMessage().getFormattedMessage();
    if (desensitionFlag) {
      msg = desensitization(msg);
    }
    if (lengthLimit > 0 && msg.length() > lengthLimit) {
      msg = msg.substring(0, lengthLimit) + "...";
    }
    resultBuffer.append("_msg=").append(msg);

    if (event.getThrown() != null) {
      Throwable t = event.getThrown();
      resultBuffer.append(' ').append(ExceptionUtils.getStackTrace(t));
    }
    resultBuffer.append(System.lineSeparator());
    return resultBuffer.toString();
  }

  /**
   * msg脱敏
   */
  private String desensitization(String msg) {
    if (StringUtils.isEmpty(msg)) {
      return "";
    }
    List<Node> idxList = EMPTY_LIST;
    for (String field : desensitizationManager.getDesensitizationFields()) {
      if (EMPTY_LIST == idxList) {
        idxList = newArrayList();
      }
      idxList.addAll(parseNodes(msg, field));
    }
    Node[] nodes = idxList.stream().sorted()
        .toArray(size -> new Node[size]);
    if (ArrayUtils.isEmpty(nodes)) {
      return msg;
    }

    char[] charArr = msg.toCharArray();
    int currentCharArrIdx = 0;
    int currentNodeIdx = 0;
    int nodeLength = nodes.length;
    Node currentNode = null;
    Node nextNode = null;
    StringBuilder result = new StringBuilder();

    for (currentNodeIdx = 0; currentNodeIdx < nodeLength; currentNodeIdx++) {
      currentNode = nodes[currentNodeIdx];
      if (currentNodeIdx == nodeLength - 1) {
        nextNode = null;
      } else {
        nextNode = nodes[currentNodeIdx + 1];
      }
      Pair<Integer, Integer> rangePair = getFieldRange(charArr, currentNode, nextNode);
      int payloadStartIdx = rangePair.getLeft();
      int payloadEndIdx = rangePair.getRight();
      if (payloadStartIdx == payloadEndIdx) {
        int boundary = null == nextNode ? charArr.length : nextNode.getIndex();
        int charCount = boundary - currentCharArrIdx;
        result.append(new String(charArr, currentCharArrIdx, charCount));
        currentCharArrIdx = boundary;
        continue;
      }
      int charCount = payloadStartIdx - currentCharArrIdx;
      if (charCount < 1) {
        //字符个数小于1，继续下一个
        continue;
      }
      result.append(new String(charArr, currentCharArrIdx, charCount));

      String payload = new String(charArr, payloadStartIdx,
          payloadEndIdx - payloadStartIdx);
      //进行脱敏
      payload = desensitizationManager.desensitization(payload, currentNode.getSearchStr());
      result.append(payload);
      currentCharArrIdx = payloadEndIdx;
    }
    if (currentCharArrIdx <= charArr.length - 1) {
      result.append(new String(charArr, currentCharArrIdx,
          charArr.length - currentCharArrIdx));
    }
    return result.toString();

  }

  private Pair<Integer, Integer> getFieldRange(char[] charArr, Node node, Node nextNode) {
    int start = 0;
    int end = 0;
    int idx = node.getIndex() + node.getSearchStrLength();
    int boundary = null == nextNode ? charArr.length : nextNode.getIndex();
    boolean inPattern = false;
    boolean escaped = false;
    for (; idx <= boundary; idx++) {
      if (idx == boundary) {
        end = idx;
        break;
      }
      char c = charArr[idx];
      if (isEscapeToken(c)) {
        escaped = true;
        if (inPattern) {
          end = idx;
          break;
        } else {
          continue;
        }
      }
      if (isTerminalToken(c)) {
        if (inPattern) {
          end = idx;
          break;
        } else {
          start = end = 0;
          break;
        }
      }
      if (inPattern) {
        end = idx;
      } else {
        if (escaped) {
          inPattern = true;
          start = idx;
        } else {
          return new ImmutablePair<>(0, 0);
        }
      }
    }

    return new ImmutablePair<>(start, end);
  }

  private boolean isEscapeToken(char c) {
    return desensitizationManager.isEscapeToken(c);
  }

  private boolean isTerminalToken(char c) {
    return desensitizationManager.isTerminalToken(c);
  }


  private List<Node> parseNodes(final CharSequence str,
      final CharSequence fieldName) {
    List<Node> result = EMPTY_LIST;

    int searchStrLen = length(fieldName);
    int totalLen = length(str);
    int firstIdx = indexOf(str, fieldName);
    if (firstIdx < 0) {
      return result;
    }
    int secondIdx = indexOf(str, fieldName,
        firstIdx + searchStrLen);
    if (secondIdx < 0) {
      return newArrayList(new Node(firstIdx, fieldName));
    }
    result = newArrayList(new Node(firstIdx, fieldName),
        new Node(secondIdx, fieldName));

    int nextPos = secondIdx + searchStrLen;
    int currentIdx = -1;
    while ((currentIdx = indexOf(str, fieldName, nextPos)) >= 0) {
      nextPos = currentIdx + searchStrLen;
      result.add(new Node(currentIdx, fieldName));
      if (nextPos > totalLen - searchStrLen) {
        break;
      }
    }

    return result;
  }

  /**
   * 解析msg中需要脱敏的字段
   */
  public static class Node implements Comparable<Node> {

    int index;
    CharSequence searchStr;
    int searchStrLength;

    public Node(int index, CharSequence searchStr) {
      this.index = index;
      this.searchStr = searchStr;
      searchStrLength = searchStr.length();
    }

    public int getIndex() {
      return index;
    }

    public String getSearchStr() {
      return searchStr.toString();
    }

    public int getSearchStrLength() {
      return searchStrLength;
    }

    @Override
    public int compareTo(Node o) {
      if (this == o) {
        return 0;
      }
      if (null == o) {
        return 1;
      }
      return this.index - o.index;
    }

    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder("Node{");
      sb.append("index=").append(index);
      sb.append(", searchStr=").append(searchStr);
      sb.append('}');
      return sb.toString();
    }
  }


}
